﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;

namespace GenerateAccessors
{
	class Program
	{
		static void Main(string[] args)
		{
			if (args.Length != 1)
			{
				Console.WriteLine("Usage: GenerateAccessors.exe <Assembly>");
				return;
			}

			AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += new ResolveEventHandler(CurrentDomain_ReflectionOnlyAssemblyResolve);

			//var e = new CodeMemberEvent();
			//e.Attributes = MemberAttributes.Final | MemberAttributes.Public;
			//e.Name = "TestEvent";
			//e.Type = new CodeTypeReference(typeof(EventHandler<EventArgs>));

			//CodeDomProvider.CreateProvider("C#").GenerateCodeFromMember(e, Console.Out, null);
			//Console.ReadKey();

			new Program().Run(args[0]);
		}

		static Assembly CurrentDomain_ReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs args)
		{
			return Assembly.ReflectionOnlyLoad(args.Name);
		}

		CodeTypeDeclaration ProcessType(CodeDomProvider provider, Type type)
		{
			var typeDecl = Code.CreateTypeDeclaration(type);

			BindingFlags bindingFlags
				= BindingFlags.Public
				| BindingFlags.NonPublic
				| BindingFlags.Instance
				| BindingFlags.Static
				| BindingFlags.DeclaredOnly;

			foreach (var info in type.GetNestedTypes(bindingFlags))
			{
				if (!provider.IsValidIdentifier(info.Name))
					continue;

				typeDecl.Members.Add(ProcessType(provider, info));
			}

			foreach (var info in type.GetFields(bindingFlags))
			{
				// exclude backing fields and other compiler generated stuff
				// TODO: test generics
				if (!provider.IsValidIdentifier(info.Name))
					continue;

				typeDecl.Members.Add(Code.CreateMemberProperty(info));
			}

			foreach (var info in type.GetProperties(bindingFlags))
			{
				if (!provider.IsValidIdentifier(info.Name))
					continue;

				typeDecl.Members.Add(Code.CreateMemberProperty(info));
			}

			foreach (var info in type.GetMethods(bindingFlags))
			{
				if (!provider.IsValidIdentifier(info.Name))
					continue;

				if (info.IsPropertyAccessor(type.GetProperties(bindingFlags)))
					continue;

				if (info.HasByRefParameters())
					continue;

				typeDecl.Members.Add(Code.CreateMemberMethod(info));
			}

			bindingFlags
				= BindingFlags.Public
				| BindingFlags.NonPublic
				| BindingFlags.Instance
				| BindingFlags.DeclaredOnly;

			foreach (var info in type.GetConstructors(bindingFlags))
			{
				typeDecl.Members.Add(Code.CreateConstructor(info));
			}

			return typeDecl;
		}

		void Run(string strAssembly)
		{
			var assembly = Assembly.ReflectionOnlyLoadFrom(strAssembly);

			//var assembly = Assembly.LoadFile(System.IO.Directory.GetCurrentDirectory() + @"\" + strAssembly);
			//var assembly = Assembly.LoadFile(strAssembly)

			var provider = CodeDomProvider.CreateProvider("C#");
			var compileUnits = new List<CodeCompileUnit>();

			foreach (var type in assembly.GetTypes())
			{
				if (!type.IsClass)
					continue;

				// Ignore generic types for now.
				if (type.IsGenericType)
					continue;

				// Ignore nested classes in the main loop, they are processed as part of outer class.
				if (type.IsNested)
					continue;

				// Ignore other weird stuff too (for example for LINQ compiler generates debug views)
				if (!provider.IsValidIdentifier(type.Name))
					continue;

				var typeNamespace = new CodeNamespace(type.Namespace);

				// this doesnt help at all, still the types FullName is emitted everywhere
				//typeNamespace.Imports.Add(new CodeNamespaceImport("System"));
				//typeNamespace.Imports.Add(new CodeNamespaceImport("System.Reflection"));

				var typeDecl = ProcessType(provider, type);

				typeNamespace.Types.Add(typeDecl);

				var compileUnit = new CodeCompileUnit();

				compileUnit.Namespaces.Add(typeNamespace);
				compileUnits.Add(compileUnit);

#if DEBUG
				if (strAssembly == "TestClassLibrary.dll")
				{
					var codeGenOptions = new CodeGeneratorOptions();
					codeGenOptions.IndentString = "\t";
					codeGenOptions.BracingStyle = "Block"; // "C" or "Block"
					using (var writer = new StreamWriter(@"..\..\..\TestClassLibrary_Accessors\Generated\" + typeDecl.Name + ".cs"))
						provider.GenerateCodeFromCompileUnit(compileUnit, writer, codeGenOptions);
				}
#endif
			}

			var parameters = new CompilerParameters();

			parameters.WarningLevel = 4;
			//parameters.TreatWarningsAsErrors = true;
			//parameters.TempFiles = new TempFileCollection(@"C:\temp", true);
			parameters.OutputAssembly = strAssembly.Substring(0, strAssembly.Length - 4) + "_Accessors.dll";

			foreach (var assemblyName in assembly.GetReferencedAssemblies().Union(new AssemblyName[] { assembly.GetName() }))
			{
				var assembly2 = Assembly.Load(assemblyName);

				foreach (var module in assembly2.GetModules())
				{
					parameters.ReferencedAssemblies.Add(module.FullyQualifiedName);
				}
			}

			var results = provider.CompileAssemblyFromDom(parameters, compileUnits.ToArray());

			// If there are any errors, compile again to store temp files.
			if (results.Errors.Count != 0)
			{
				parameters.TempFiles = new TempFileCollection(System.IO.Directory.GetCurrentDirectory(), true);
				results = provider.CompileAssemblyFromDom(parameters, compileUnits.ToArray());

				foreach (var error in results.Errors)
				{
					Debug.WriteLine(error);
					Console.WriteLine(error);
				}
			}
		}
	}

//PrivateTestClass.PrivateMethod: Private
//PrivateTestClass.ProtectedMethod: Family
//PrivateTestClass.PublicMethod: Public
//PrivateTestClass.InternalMethod: Assembly
//PrivateTestClass.ProtectedInternalMethod: FamORAssem
//PublicTestClass.PrivateMethod: Private
//PublicTestClass.ProtectedMethod: Family
//PublicTestClass.PublicMethod: Public
//PublicTestClass.InternalMethod: Assembly
//PublicTestClass.ProtectedInternalMethod: FamORAssem
}
